﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;
using Kent.Boogaart.HelperTrinity;

namespace WPFCABShell.$safeprojectname$.Diagnostics
{
	/// <summary>
	/// Provides a thin wrapper around the <see cref="TraceSource"/> class to simplify logging code.
	/// </summary>
	/// <remarks>
	/// <para>
	/// The <c>Log</c> class provides a useful wrapper around the <see cref="TraceSource"/> class. It does so to simplify the
	/// code required to log messages.
	/// </para>
	/// <para>
	/// Instances of <c>Log</c> can be created based on the name of a <see cref="Type"/> (<see cref="CreateForType"/>),
	/// <see cref="Assembly"/> (<see cref="CreateForAssembly"/>) or namespace (<see cref="CreateForNamespace"/>). Alternatively,
	/// the <see cref="CreateNamed"/> method allows an arbitrary name to be used.
	/// </para>
	/// <para>
	/// The <see cref="Debug"/>, <see cref="Verbose"/>, <see cref="Information"/>, <see cref="Warning"/> and <see cref="Error"/>
	/// methods can be used to log messages. The <see cref="Debug"/> overloads are conditionally compiled for debug builds only.
	/// All of these methods have overloads that accept a format string and arguments.
	/// </para>
	/// <para>
	/// In addition, the <see cref="Write"/> method provides a general means of logging at any specified level. This can be
	/// useful where the log level is not known until runtime, or when the required level has no corresponding method such as
	/// the <see cref="LogLevel.Critical"/> level.
	/// </para>
	/// <para>
	/// The <see cref="IsVerboseEnabled"/>, <see cref="IsInformationEnabled"/>, <see cref="IsWarningEnabled"/> and
	/// <see cref="IsErrorEnabled"/> properties can be used to determine whether a specified log level is enabled. There is no
	/// <c>IsDebugEnabled</c> property, since that is determined at compile-time based on the <c>DEBUG</c> compiler symbol.
	/// </para>
	/// <para>
	/// In addition, the <see cref="ShouldWrite"/> method provides a general means of determining whether a specified log level
	/// is enabled. This can be useful where the log level is not known until runtime, or when the required level has no
	/// corresponding method such as the <see cref="LogLevel.Critical"/> level.
	/// </para>
	/// <para>
	/// The <see cref="Performance"/> methods can be used to measure the performance of a block of code. These methods return
	/// an <see cref="IDisposable"/> instance that will automatically log performance information when disposed.
	/// </para>
	/// <para>
	/// The <see cref="PushLogicalOperation"/> and <see cref="PopLogicalOperation"/> methods can be used to begin and end
	/// logical operations. <see cref="PushLogicalOperation"/> returns an <see cref="IDisposable"/> implementation that will
	/// automatically invoke <see cref="PopLogicalOperation"/> when disposed. As such, it can be used effectively from a
	/// <c>using</c> block.
	/// </para>
	/// <para>
	/// Logical operations are maintained in a stack. The stack of logical operations in progress can optionally be included in
	/// each logged message (except for debug messages). This allows you to diagnose the logical operations that were in progress
	/// when a message was logged.
	/// </para>
	/// </remarks>
	/// <example>
	/// The following code shows how a <c>Log</c> instance can be created for a specified type:
	/// <code>
	/// namespace MyNamespace
	/// {
	///		public class MyClass
	///		{
	///			private static readonly Log _log = Log.CreateForType(typeof(MyClass));
	///		}
	/// }
	/// </code>
	/// </example>
	/// <example>
	/// The following code shows how a <c>Log</c> instance can be created for use by an assembly:
	/// <code>
	/// namespace MyNamespace
	/// {
	///		internal class MyClass
	///		{
	///			internal static readonly Log Log = Log.CreateForAssembly(typeof(MyClass));
	///		}
	/// }
	/// </code>
	/// </example>
	/// <example>
	/// The following code logs some information to the log:
	/// <code>
	/// _log.Information("Here is some information.");
	/// </code>
	/// </example>
	/// <example>
	/// The following code logs some formatted information to the log:
	/// <code>
	/// _log.Information("Here is some information with a parameter at the end: {0}.", someValue);
	/// </code>
	/// </example>
	/// <example>
	/// The following code checks whether information will be logged before it attempts to do so:
	/// <code>
	/// if (_log.IsInformationEnabled)
	/// {
	///		_log.Information("Here is some information with parameters: {0}, {1}, {2}.", val1, val2, val3);
	/// }
	/// </code>
	/// </example>
	/// <example>
	/// The following code logs a critical message:
	/// <code>
	///	_log.Write(LogLevel.Critical, "Some critical message.");
	/// </code>
	/// </example>
	/// <example>
	/// The following code measures the performance of a block of code:
	/// <code>
	///	using (_log.Performance("Some code I want measured"))
	/// {
	///		//code to measure goes here
	/// }
	/// </code>
	/// </example>
	/// <example>
	/// The following code begins a logical operation and logs from inside and outside the operation:
	/// <code>
	///	using (_log.PushLogicalOperation("My Operation"))
	/// {
	///		_log.Information("We're inside the logical operation.");
	/// }
	/// 
	/// _log.Information("Now we're outside the logical operation.");
	/// </code>
	/// </example>
	/// <example>
	/// The following shows some example configuration for the <c>&lt;system.diagnostics&gt;</c> subsystem, assuming the
	/// existence of the class <c>MyClass</c> defined in the example above:
	/// <code>
	/// <![CDATA[
	/// <system.diagnostics>
	///		<sources>
	///			<source name="MyNamespace.MyClass"
	///					switchName="MyNamespace.MyClass" 
	///					switchType="System.Diagnostics.SourceSwitch">
	///				<listeners>
	///					<add name="listener"/>
	///				</listeners>
	/// 	</source>
	///		</sources>
	///		<switches>
	///			<add name="MyNamespace.MyClass" value="Verbose"/>
	///		</switches>
	///		<sharedListeners>
	///			<add name="listener" 
	///					type="System.Diagnostics.ConsoleTraceListener" 
	///					initializeData="false"
	///					traceOutputOptions="DateTime,ThreadId,Timestamp,LogicalOperationStack"/>
	///		</sharedListeners>
	///		<trace autoflush="true">
	///			<listeners>
	///				<add name="listener"/>
	///			</listeners>
	///		</trace>
	/// </system.diagnostics>
	/// ]]>
	/// </code>
	/// </example>
	public sealed class Log
	{
		private readonly TraceSource _traceSource;
		//accessing the correlation manager via Trace.CorrelationManager is not thread safe, so we need our own lock
		private static readonly object _correlationManagerLock = new object();
		//defines the accepted log levels - used to increase the performance of argument checking
		private static readonly LogLevel[] _logLevels = new LogLevel[] { LogLevel.Critical, LogLevel.Error, LogLevel.Information, LogLevel.Verbose, LogLevel.Warning };
		private static readonly LogCollection _logs = new LogCollection();

		/// <summary>
		/// Gets a collection of all logs.
		/// </summary>
		public static LogCollection Logs
		{
			get
			{
				return _logs;
			}
		}

		/// <summary>
		/// Gets the <see cref="TraceSource"/> that this <c>Log</c> wraps.
		/// </summary>
		/// <remarks>
		/// This property facilitates unit testing.
		/// </remarks>
		public TraceSource TraceSource
		{
			get
			{
				return _traceSource;
			}
		}

		/// <summary>
		/// Gets the name of the underlying <see cref="TraceSource"/>.
		/// </summary>
		/// <remarks>
		/// This name is used to configure the <see cref="TraceSource"/> in the <c>&lt;system.diagnostics&gt;</c> section of the
		/// application's configuration file.
		/// </remarks>
		public string Name
		{
			get
			{
				return _traceSource.Name;
			}
		}

		/// <summary>
		/// Gets a value indicating whether <see cref="LogLevel.Verbose"/> messages will be logged.
		/// </summary>
		public bool IsVerboseEnabled
		{
			get
			{
				return _traceSource.Switch.ShouldTrace(TraceEventType.Verbose);
			}
		}

		/// <summary>
		/// Gets a value indicating whether <see cref="LogLevel.Information"/> messages will be logged.
		/// </summary>
		public bool IsInformationEnabled
		{
			get
			{
				return _traceSource.Switch.ShouldTrace(TraceEventType.Information);
			}
		}

		/// <summary>
		/// Gets a value indicating whether <see cref="LogLevel.Warning"/> messages will be logged.
		/// </summary>
		public bool IsWarningEnabled
		{
			get
			{
				return _traceSource.Switch.ShouldTrace(TraceEventType.Warning);
			}
		}

		/// <summary>
		/// Gets a value indicating whether <see cref="LogLevel.Error"/> messages will be logged.
		/// </summary>
		public bool IsErrorEnabled
		{
			get
			{
				return _traceSource.Switch.ShouldTrace(TraceEventType.Error);
			}
		}

		/// <summary>
		/// Creates an instance of <c>Log</c> for the specified type.
		/// </summary>
		/// <param name="type">
		/// The type for which the log is created.
		/// </param>
		/// <returns>
		/// The <c>Log</c> instance.
		/// </returns>
		public static Log CreateForType(Type type)
		{
			ArgumentHelper.AssertNotNull(type, "type");
			return new Log(type.FullName);
		}

		/// <summary>
		/// Creates an instance of <c>Log</c> for the namespace of the specified type.
		/// </summary>
		/// <param name="type">
		/// The type whose namespace is used to create the log.
		/// </param>
		/// <returns>
		/// The <c>Log</c> instance.
		/// </returns>
		public static Log CreateForNamespace(Type type)
		{
			ArgumentHelper.AssertNotNull(type, "type");
			return new Log(type.Namespace);
		}

		/// <summary>
		/// Creates an instance of <c>Log</c> for the assembly of the specified type.
		/// </summary>
		/// <param name="type">
		/// The type whose assembly is used to create the log.
		/// </param>
		/// <returns>
		/// The <c>Log</c> instance.
		/// </returns>
		public static Log CreateForAssembly(Type type)
		{
			ArgumentHelper.AssertNotNull(type, "type");
			return CreateForAssembly(type.Assembly);
		}

		/// <summary>
		/// Creates an instance of <c>Log</c> for the specified assembly.
		/// </summary>
		/// <param name="assembly">
		/// The assembly for which the log is created.
		/// </param>
		/// <returns>
		/// The <c>Log</c> instance.
		/// </returns>
		public static Log CreateForAssembly(Assembly assembly)
		{
			ArgumentHelper.AssertNotNull(assembly, "assembly");
			return new Log(assembly.GetName().Name);
		}

		/// <summary>
		/// Creates an instance of <c>Log</c> with an arbitrary name.
		/// </summary>
		/// <param name="name">
		/// The name for the log.
		/// </param>
		/// <returns>
		/// The <c>Log</c> instance.
		/// </returns>
		public static Log CreateNamed(string name)
		{
			ArgumentHelper.AssertNotNull(name, "name");
			return new Log(name);
		}

		private Log(string name)
		{
			_traceSource = new TraceSource(name);
			//maintain a collection of all logs to help with diagnostics
			_logs.Add(this);
		}

		/// <summary>
		/// Logs a debug message.
		/// </summary>
		/// <remarks>
		/// This method is conditional on the <c>DEBUG</c> compiler symbol. Therefore calls to the method will only be included
		/// in debug builds.
		/// </remarks>
		/// <param name="message">
		/// The message to log.
		/// </param>
		[Conditional("DEBUG")]
		[SuppressMessage("Microsoft.Performance", "CA1822", Justification = "The member is supposed to be access via a Log instance, even if that is not strictly required.")]
		public void Debug(string message)
		{
			ArgumentHelper.AssertNotNull(message, "message");
			System.Diagnostics.Debug.WriteLine(message);
		}

		/// <summary>
		/// Logs a debug message.
		/// </summary>
		/// <remarks>
		/// This method is conditional on the <c>DEBUG</c> compiler symbol. Therefore calls to the method will only be included
		/// in debug builds.
		/// </remarks>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		[Conditional("DEBUG")]
		[SuppressMessage("Microsoft.Performance", "CA1822", Justification = "The member is supposed to be access via a Log instance, even if that is not strictly required.")]
		public void Debug(string format, params object[] args)
		{
			ArgumentHelper.AssertNotNull(format, "format");
			ArgumentHelper.AssertNotNull(args, "args");
			System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, format, args));
		}

		/// <summary>
		/// Logs a verbose message.
		/// </summary>
		/// <param name="message">
		/// The message to log.
		/// </param>
		public void Verbose(string message)
		{
			Write(LogLevel.Verbose, message);
		}

		/// <summary>
		/// Logs a verbose message.
		/// </summary>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		public void Verbose(string format, params object[] args)
		{
			Write(LogLevel.Verbose, format, args);
		}

		/// <summary>
		/// Logs an informational message.
		/// </summary>
		/// <param name="message">
		/// The message to log.
		/// </param>
		public void Information(string message)
		{
			Write(LogLevel.Information, message);
		}

		/// <summary>
		/// Logs an informational message.
		/// </summary>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		public void Information(string format, params object[] args)
		{
			Write(LogLevel.Information, format, args);
		}

		/// <summary>
		/// Logs a warning message.
		/// </summary>
		/// <param name="message">
		/// The message to log.
		/// </param>
		public void Warning(string message)
		{
			Write(LogLevel.Warning, message);
		}

		/// <summary>
		/// Logs a warning message.
		/// </summary>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		public void Warning(string format, params object[] args)
		{
			Write(LogLevel.Warning, format, args);
		}

		/// <summary>
		/// Logs an error message.
		/// </summary>
		/// <param name="message">
		/// The message to log.
		/// </param>
		public void Error(string message)
		{
			Write(LogLevel.Error, message);
		}

		/// <summary>
		/// Logs an error message.
		/// </summary>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		public void Error(string format, params object[] args)
		{
			Write(LogLevel.Error, format, args);
		}

		/// <summary>
		/// Logs the details of an exception at the <see cref="LogLevel.Error"/> log level.
		/// </summary>
		/// <param name="ex">
		/// The exception whose details are to be logged.
		/// </param>
		public void Exception(Exception ex)
		{
			Exception(LogLevel.Error, ex);
		}

		/// <summary>
		/// Logs the details of an exception at a specified <see cref="LogLevel"/>.
		/// </summary>
		/// <param name="level">
		/// The <see cref="LogLevel"/> at which the exception details will be logged.
		/// </param>
		/// <param name="ex">
		/// The exception whose details are to be logged.
		/// </param>
		public void Exception(LogLevel level, Exception ex)
		{
			ArgumentHelper.AssertNotNull(ex, "ex");
			ArgumentHelper.AssertEnumMember(level, "level");
			StringBuilder sb = new StringBuilder();

			while (ex != null)
			{
				sb.Append(ex.GetType().FullName).Append(" : ").AppendLine(ex.Message);
				sb.AppendLine(ex.StackTrace);

				ex = ex.InnerException;

				if (ex != null)
				{
					sb.AppendLine("--- INNER EXCEPTION ---");
				}
			}

			Write(level, sb.ToString());
		}

		/// <summary>
		/// Obtains an <see cref="IDisposable"/> instance that will log the performance of a block of code when disposed.
		/// </summary>
		/// <param name="message">
		/// The message to log along with the performance information.
		/// </param>
		/// <returns>
		/// An <see cref="IDisposable"/> that will log performance information when disposed.
		/// </returns>
		public IDisposable Performance(string message)
		{
			ArgumentHelper.AssertNotNullOrEmpty(message, "message", true);
			return new PerformanceLogger(this, message);
		}

		/// <summary>
		/// Obtains an <see cref="IDisposable"/> instance that will log the performance of a block of code when disposed.
		/// </summary>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		/// <returns>
		/// An <see cref="IDisposable"/> that will log performance information when disposed.
		/// </returns>
		public IDisposable Performance(string format, params object[] args)
		{
			ArgumentHelper.AssertNotNull(format, "format");
			ArgumentHelper.AssertNotNull(args, "args");
			return Performance(string.Format(CultureInfo.InvariantCulture, format, args));
		}

		/// <summary>
		/// Determines whether messages of the specified <see cref="LogLevel"/> will be written.
		/// </summary>
		/// <param name="level">
		/// The log level.
		/// </param>
		/// <returns>
		/// <see langword="true"/> if messages of the specified level will be logged, otherwise <see langword="false"/>.
		/// </returns>
		public bool ShouldWrite(LogLevel level)
		{
			ArgumentHelper.AssertEnumMember(level, "level", _logLevels);
			return _traceSource.Switch.ShouldTrace((TraceEventType)level);
		}

		/// <summary>
		/// Writes a message at the specified level.
		/// </summary>
		/// <param name="level">
		/// The level at which the message will be logged.
		/// </param>
		/// <param name="message">
		/// The message to log.
		/// </param>
		public void Write(LogLevel level, string message)
		{
			ArgumentHelper.AssertEnumMember(level, "level", _logLevels);
			ArgumentHelper.AssertNotNull(message, "message");
			_traceSource.TraceEvent((TraceEventType)level, 0, message);
		}

		/// <summary>
		/// Writes a formatted message at the specified level.
		/// </summary>
		/// <param name="level">
		/// The level at which the message will be logged.
		/// </param>
		/// <param name="format">
		/// The message format to log.
		/// </param>
		/// <param name="args">
		/// Arguments to be substituted into <paramref name="format"/> prior to logging.
		/// </param>
		public void Write(LogLevel level, string format, params object[] args)
		{
			ArgumentHelper.AssertEnumMember(level, "level", _logLevels);
			ArgumentHelper.AssertNotNull(format, "format");
			ArgumentHelper.AssertNotNull(args, "args");
			_traceSource.TraceEvent((TraceEventType)level, 0, format, args);
		}

		/// <summary>
		/// Pushes a named logical operation onto the logical operation stack.
		/// </summary>
		/// <remarks>
		/// This method can be used to add a new logical operation to the logical operation stack. This stack can optionally be
		/// included in any log messages of any level (except for debug messages).
		/// </remarks>
		/// <param name="operationName">
		/// The name of the logical operation to begin.
		/// </param>
		/// <returns>
		/// An <see cref="IDisposable"/> implementation that will automatically invoke <see cref="PopLogicalOperation"/> when
		/// disposed.
		/// </returns>
		[SuppressMessage("Microsoft.Performance", "CA1822", Justification = "The member is supposed to be access via a Log instance, even if that is not strictly required.")]
		public IDisposable PushLogicalOperation(string operationName)
		{
			ArgumentHelper.AssertNotNullOrEmpty(operationName, "operationName", true);

			lock (_correlationManagerLock)
			{
				System.Diagnostics.Trace.CorrelationManager.StartLogicalOperation(operationName);
			}

			return new LogicalOperationClosure(this);
		}

		/// <summary>
		/// Pushes a named logical operation onto the logical operation stack.
		/// </summary>
		/// <param name="format">
		/// The format of the operation name.
		/// </param>
		/// <param name="args">
		/// Any arguments to substitute into the operation name.
		/// </param>
		/// <returns>
		/// An <see cref="IDisposable"/> implementation that will automatically invoke <see cref="PopLogicalOperation"/> when
		/// disposed.
		/// </returns>
		[SuppressMessage("Microsoft.Performance", "CA1822", Justification = "The member is supposed to be access via a Log instance, even if that is not strictly required.")]
		public IDisposable PushLogicalOperation(string format, params object[] args)
		{
			ArgumentHelper.AssertNotNull(format, "format");
			ArgumentHelper.AssertNotNull(args, "args");
			return PushLogicalOperation(string.Format(CultureInfo.InvariantCulture, format, args));
		}

		/// <summary>
		/// Pops the last logical operation off the logical operation stack.
		/// </summary>
		/// <exception cref="InvalidOperationException">
		/// If there are no logical operations on the stack to be popped.
		/// </exception>
		[SuppressMessage("Microsoft.Performance", "CA1822", Justification = "The member is supposed to be access via a Log instance, even if that is not strictly required.")]
		public void PopLogicalOperation()
		{
			lock (_correlationManagerLock)
			{
				System.Diagnostics.Trace.CorrelationManager.StopLogicalOperation();
			}
		}

		private sealed class LogicalOperationClosure : IDisposable
		{
			private readonly Log _log;
			private bool _disposed;

			public LogicalOperationClosure(Log log)
			{
				_log = log;
			}

			public void Dispose()
			{
				if (!_disposed)
				{
					_disposed = true;
					_log.PopLogicalOperation();
				}
			}
		}

		private sealed class PerformanceLogger : IDisposable
		{
			private Log _log;
			private string _message;
			private Stopwatch _stopwatch;
			private bool _disposed;

			public PerformanceLogger(Log log, string message)
			{
				System.Diagnostics.Debug.Assert(log != null);
				System.Diagnostics.Debug.Assert(message != null);
				_log = log;
				_message = message;
				_stopwatch = Stopwatch.StartNew();
			}

			public void Dispose()
			{
				if (!_disposed)
				{
					_stopwatch.Stop();
					_disposed = true;
					_log.Information("'{0}' took {1} to execute ({2}ms)", _message, _stopwatch.Elapsed, _stopwatch.ElapsedMilliseconds);
				}
			}
		}

		/// <summary>
		/// Implements an observable collection of logs.
		/// </summary>
		public sealed class LogCollection : ObservableCollection<Log>
		{
			/// <summary>
			/// Gets a log by its owning type.
			/// </summary>
			/// <param name="type">
			/// The type passed to <see cref="Log.CreateForType"/> when creating the log.
			/// </param>
			/// <returns>
			/// The <see cref="Log"/> instance, or <see langword="null"/> if the log couldn't be found.
			/// </returns>
			public Log this[Type type]
			{
				get
				{
					return this[type.FullName];
				}
			}

			/// <summary>
			/// Gets a log by its name.
			/// </summary>
			/// <param name="name">
			/// The name of the log.
			/// </param>
			/// <returns>
			/// The <see cref="Log"/> instance, or <see langword="null"/> if the log couldn't be found.
			/// </returns>
			public Log this[string name]
			{
				get
				{
					foreach (Log log in this)
					{
						if (string.CompareOrdinal(name, log.Name) == 0)
						{
							return log;
						}
					}

					return null;
				}
			}
		}
	}
}
